﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using com.hetaoos.PacMaker.Utils;
using System.IO;
using System.Text.RegularExpressions;
using System.Globalization;
using com.hetaoos.ForumSpider.Utils;

#region Copyright (C) 2010 Null Studio
/*
 * Author:Null hetaoos@gmail.com
 * Blog: http://blog.hetaoos.com
 * Copyright (C) 2010 Null Studio
 * 
 * Reference:
 * http://igfw.tk/archives/286
 * https://autoproxy.org/zh-CN/Rules
 * https://github.com/iamamac/autoproxy2pac
 * 
 * PACMaker is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * PACMaker is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PACMaker. If not, see <http://www.gnu.org/licenses/>.
 */
#endregion

namespace com.hetaoos.PacMaker.Maker
{
    public class PACMaker
    {
        private static readonly char[] splitTag = new char[] { '\r', '\n' };
        private const string defaultUrl = @"http://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt";
        private const string defaultProxy = @"PROXY 127.0.0.1:7070; SOCKS5 127.0.0.1:7070; PROXY 127.0.0.1:8580";
        private HttpClient client = new HttpClient();

        public string GwfList { get; set; }

        public bool UseSystemWebProxy
        {
            get { return client.UseSystemWebProxy; }
            set { client.UseSystemWebProxy = value; }

        }

        #region 原始的文件
        private string[] GetRuleSource_All()
        {
            //var ds = File.ReadAllText("Http.pac");
            //int i = ds.IndexOf(@"decode64(""");
            //ds = ds.Substring(i + 10, ds.Length - i - 10 - 3);
            //ds = Encoding.Default.GetString(Convert.FromBase64String(ds));

            //下载Base64编码的源文件
            string data = client.DownloadString("http://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt");//HttpHelper.Get("http://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt");
            //解码
            data = Encoding.Default.GetString(Convert.FromBase64String(data));
            //分行，并移除空行
            var lines = data.Split(splitTag, StringSplitOptions.RemoveEmptyEntries);
            //移除行两边的空格、选取非空行且不以!开头的行、跳过第一行版本注释
            lines = lines.Select(o => o.Trim()).Where(o => o.Length > 0 && o[0] != '!').Skip(1).ToArray();
            bool useProxy;
            string line;
            Dictionary<string, bool> domains = new Dictionary<string, bool>();
            Dictionary<string, bool> startWith = new Dictionary<string, bool>();
            Dictionary<string, bool> containsWith = new Dictionary<string, bool>();
            Dictionary<string, bool> regexWith = new Dictionary<string, bool>();
            foreach (string item in lines)
            {
                useProxy = !item.StartsWith("@@");
                line = useProxy ? item : item.Substring(2);

                if (line.StartsWith("/") && line.EndsWith("/"))//直接正则表达式
                {
                    //line = line.Substring(1, line.Length - 2);
                    if (regexWith.Keys.Contains(line) == false)
                    {
                        regexWith.Add(line, useProxy);
                    }
                }
                else
                {
                    #region 注释
                    /* js 正则表达式 /i 不区分大小写
                     * *-->.*
                     * /-->\/
                     * .-->\.
                     * ?-->\?
                     * %-->\%
                     * --->\-
                     * =-->\=
                     * &-->\&
                     * \-->\\
                     * { [ ( | ) * + ? \ 
                     * HTTP 开头 ^[\w\-]+:\/+(?!\/)(?:[^\/]+\.)?
                     * ^ 结尾 (?:[^\w\-.%\u0080-\uFFFF]|$)
                     */
                    #endregion
                    if (line.StartsWith("||"))//必须是域名
                    {
                        #region 注释
                        /*  ||example.com
                         * 这种规则匹配的是 example.com 及其子域名，
                         * 如 http://example.com/foo 和 http://subdomain.example.com/bar
                         * ，但不会匹配在 URL 其它位置出现 example.com 的情况。
                         * 一般用于该站点的 IP 被封锁的情况。
                         * ||173ng.com
                         * /^[\w\-]+:\/+(?!\/)(?:[^\/]+\.)?173ng\.com/i
                        */
                        #endregion
                        line = line.Substring(2);
                        line = line.ToLower().Trim();
                        line = line.Trim('.', '*', '/', '\\', ' ', '|');
                        if (domains.Keys.Contains(line) == false)
                        {
                            domains.Add(line, useProxy);
                        }
                    }
                    else
                    {
                        if (line.StartsWith("|"))//开头匹配
                        {
                            #region 注释
                            /* |https://ssl.example.com
                             * 这种规则匹配的是所有以 https://ssl.example.com 开头的地址。
                             * 一般用于某些 IP 的 HTTPS 访问被定点封锁的情况，也用于暂时应付 issue 117。
                            */
                            #endregion
                            line = line.Substring(1);
                            line = line.Trim();
                            line = EscapeRule(line);
                            if (startWith.Keys.Contains(line) == false)
                            {
                                startWith.Add(line, useProxy);
                            }

                        }
                        else//包含
                        {
                            #region 注释
                            /*
                             * example.com
                             * 匹配：http://www.example.com/foo
                             * 匹配：http://www.google.com/search?q=www.example.com
                             * 不匹配：https://www.example.com/
                             * 用于表明字符串 example.com 为 URL 关键词。任何含关键词的 http 连接（不包括 https）皆会使用代理。
                             * 173ng.com
                             * if(/173ng\.com/i.test(url)) return PROXY;
                             */
                            #endregion
                            line = line.Trim();
                            line = EscapeRule(line);
                            if (containsWith.Keys.Contains(line) == false)
                            {
                                containsWith.Add(line, useProxy);
                            }
                        }
                    }
                }
            }
            StringBuilder pac = new StringBuilder();
            pac.AppendFormat("// Date:{0:g}", DateTime.Now);
            pac.AppendLine();
            pac.AppendLine(ParseAsRegex(regexWith));
            pac.AppendLine(ParseAsStartWith(startWith));
            pac.AppendLine(ParseAsContainsWith(containsWith));
            pac.AppendLine(ParseAsDomain(domains));
            pac.AppendLine(IsDomain);
            //pac.AppendLine(string.Format(FindProxyForURL, " "));
            pac.AppendLine(FindProxyForURL.Replace("PROXY_TAG", "PROXY 127.0.0.1:7070; SOCKS5 127.0.0.1:7070; PROXY 127.0.0.1:8580"));
            var s = pac.ToString();
            File.WriteAllText(@"D:\test.pac", s);
            File.WriteAllText(@"D:\test.js", s);
            return lines;
        }
        #endregion

        private DateTime lastModified;
        public List<string> GetRuleSource(string url)
        {
            try
            {
                new Uri(url);
            }
            catch
            {
                url = defaultUrl;
            }
            //下载Base64编码的源文件
            string data = client.DownloadString(url); //HttpHelper.Get(url ?? defaultUrl);
            //解码
            data = Encoding.Default.GetString(Convert.FromBase64String(data));
            //移除规则中的引号，误添加的
            data = data.Replace(@"""", "");
            //分行，并移除空行
            var lines = data.Split(splitTag, StringSplitOptions.RemoveEmptyEntries);
            int index;
            const string dateTimeTag = "Last Modified:";
            foreach (var s in lines)
            {
                if ((index = s.IndexOf(dateTimeTag)) >= 0)
                {
                    lastModified = Convert.ToDateTime(s.Substring(index + dateTimeTag.Length).Trim());
                    break;
                }
            }
            //移除行两边的空格、选取非空行且不以!开头的行、跳过第一行版本注释
            return lines.Select(o => o.Trim()).Where(o => o.Length > 0 && o[0] != '!').Skip(1).ToList();

        }

        public List<string> ParseCustomRules(string s)
        {
            if (string.IsNullOrEmpty(s))
            {
                return new List<string>();
            }
            //分行，并移除空行
            var lines = s.Split(splitTag, StringSplitOptions.RemoveEmptyEntries);
            //移除行两边的空格、选取非空行且不以!开头的行、跳过第一行版本注释
            return lines.Select(o => o.Trim()).Where(o => o.Length > 0 && o[0] != '!').ToList();
        }

        public string ConversionToPac(IEnumerable<string> rules, string proxy, bool enableBase64Encoding)
        {
            bool useProxy;
            string line;
            Dictionary<string, bool> domains = new Dictionary<string, bool>();
            Dictionary<string, bool> startWith = new Dictionary<string, bool>();
            Dictionary<string, bool> containsWith = new Dictionary<string, bool>();
            Dictionary<string, bool> regexWith = new Dictionary<string, bool>();
            foreach (string item in rules)
            {
                useProxy = !item.StartsWith("@@");
                line = useProxy ? item : item.Substring(2);

                if (line.StartsWith("/") && line.EndsWith("/"))//直接正则表达式
                {
                    //line = line.Substring(1, line.Length - 2);
                    if (regexWith.Keys.Contains(line) == false)
                    {
                        regexWith.Add(line, useProxy);
                    }
                }
                else
                {
                    if (line.StartsWith("||"))//必须是域名
                    {
                        line = line.Substring(2);
                        line = line.ToLower().Trim();
                        line = line.Trim('.', '*', '/', '\\', ' ', '|');
                        if (domains.Keys.Contains(line) == false)
                        {
                            domains.Add(line, useProxy);
                        }
                    }
                    else if (line.StartsWith("|"))//开头匹配
                    {
                        line = line.Substring(1);
                        line = line.Trim();
                        line = EscapeRule(line);
                        if (startWith.Keys.Contains(line) == false)
                        {
                            startWith.Add(line, useProxy);
                        }

                    }
                    else//包含
                    {
                        line = line.Trim();
                        line = EscapeRule(line);
                        if (containsWith.Keys.Contains(line) == false)
                        {
                            containsWith.Add(line, useProxy);
                        }
                    }

                }
            }
            StringBuilder pac = new StringBuilder();
            CultureInfo cultureInfo = CultureInfo.CreateSpecificCulture("en-US");
            const string format = "ddd, d MMM yyyy HH:mm:ss zz00";
            pac.AppendFormat("// PAC Maker -- By Null hetaoos@gmail.com");
            pac.AppendLine();
            pac.AppendFormat("// Build Date:                {0}", DateTime.Now.ToString(format, cultureInfo));
            pac.AppendLine();
            pac.AppendFormat("// gfwlist:                   {0}", defaultUrl);
            pac.AppendLine();
            pac.AppendFormat("// gfwlist Last Modified:     {0}", lastModified.ToString(format, cultureInfo));
            pac.AppendLine();
            pac.AppendLine();
            pac.AppendLine();
            //pac.AppendLine(string.Format(FindProxyForURL, " "));"PROXY 127.0.0.1:7070; SOCKS5 127.0.0.1:7070; PROXY 127.0.0.1:8580"
            pac.AppendLine(FindProxyForURL.Replace("PROXY_TAG", proxy));
            //pac.AppendLine(FindProxyForURL);
            pac.AppendLine(IsDomain);
            pac.AppendLine(ParseAsDomain(domains));
            pac.AppendLine(ParseAsRegex(regexWith));
            pac.AppendLine(ParseAsStartWith(startWith));
            pac.AppendLine(ParseAsContainsWith(containsWith));

            if (enableBase64Encoding)
            {
                return Base64Encoder(pac.ToString());
            }
            else
            {
                return pac.ToString();
            }
        }

        private string Base64Encoder(string s)
        {
            StringBuilder sb = new StringBuilder(s.Length + 1024);
            sb.Append(Head1);
            sb.Append(Convert.ToBase64String(Encoding.Default.GetBytes(s)));
            sb.Append(Head2);
            return sb.ToString();
        }

        public bool Maker(string fileName, string proxy, string customRules, bool enableBase64Encoding)
        {
            try
            {
                var rules = GetRuleSource(GwfList);
                rules.AddRange(ParseCustomRules(customRules));
                string s = ConversionToPac(rules, proxy, enableBase64Encoding);
                File.WriteAllText(fileName, s);
                return true;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                GC.Collect();
            }
        }
        /// <summary>
        /// 初步处理源规则
        /// </summary>
        /// <param name="rule"></param>
        /// <returns></returns>
        private static string EscapeRule(string rule)
        {
            if (string.IsNullOrEmpty(rule))
            {
                return rule;
            }
            rule = Regex.Replace(rule, @"\*+", "*");
            if (rule.EndsWith("^"))
            {
                rule = string.Format(@"{0}(?:[^\w\-.%\u0080-\uFFFF]|$)", rule.Substring(0, rule.Length - 1));
            }
            if (rule.EndsWith("|"))
            {
                rule = string.Format(@"{0}$", rule.Substring(0, rule.Length - 1));
            }
            return rule;
        }

        private static string ParseAsDomain(Dictionary<string, bool> rules)
        {
            string[] rootDomains = new string[] { "com", "net", "org", "us" };
            string[] items;
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("function IsUseProxy_Domain(url, host)");
            sb.AppendLine("{");
            sb.AppendLine("    host = host.toLocaleLowerCase();");
            StringBuilder sb_White = new StringBuilder();
            StringBuilder sb_Black = new StringBuilder();

            foreach (string rootName in rootDomains)
            {
                string rootNameCheck = string.Format(".{0}", rootName);
                var temp = rules.Where(o => o.Key.EndsWith(rootNameCheck, StringComparison.CurrentCultureIgnoreCase)).ToList();
                if (temp.Count == 0)
                {
                    continue;
                }
                temp.ForEach(delegate(KeyValuePair<string, bool> item)
                {
                    rules.Remove(item.Key);
                });

                items = temp.Where(o => o.Value == false).Select(o => o.Key).ToArray();
                if (items.Length > 0)
                {
                    sb_White.AppendLine(ParseAsDomainPart(items, rootName, false));
                }
                items = temp.Where(o => o.Value == true).Select(o => o.Key).ToArray();
                if (items.Length > 0)
                {
                    sb_Black.AppendLine(ParseAsDomainPart(items, rootName, true));
                }
            }
            items = rules.Where(o => o.Value == false).Select(o => o.Key).ToArray();
            if (items.Length > 0)
            {
                sb_White.AppendLine(ParseAsDomainPart(items, string.Empty, false));
            }
            items = rules.Where(o => o.Value == true).Select(o => o.Key).ToArray();
            if (items.Length > 0)
            {
                sb_Black.AppendLine(ParseAsDomainPart(items, string.Empty, true));
            }

            sb.AppendLine(sb_White.ToString());
            sb.AppendLine(sb_Black.ToString());
            sb.AppendLine();
            sb.AppendLine("    return 0;");
            sb.AppendLine("}");
            sb.AppendLine();
            return sb.ToString();
        }
        private static string ParseAsDomainPart(string[] rules, string rootName, bool useProxy)
        {
            const string format = @"    || IsDomain(host, ""{0}"")";
            StringBuilder sb = new StringBuilder();
            if (string.IsNullOrEmpty(rootName))
            {
                sb.AppendFormat(@"    if( true && ( 0 ");
            }
            else
            {
                sb.AppendFormat(@"    if(IsDomain(host,""{0}"") && ( 0 ", rootName);
            }
            sb.AppendLine();
            foreach (string item in rules)
            {
                sb.AppendFormat(format, item.Trim().ToLower());
                sb.AppendLine();
            }
            sb.AppendLine("    ))");
            sb.AppendFormat("        return {0};", useProxy ? 1 : -1);
            sb.AppendLine();
            sb.AppendLine();
            return sb.ToString();
        }

        //IsUseProxy_Regex 使用代理 0 不使用代理 -1 未匹配 0
        private static string ParseAsRegex(Dictionary<string, bool> rules)
        {
            const string format = @"    || {0}i.test(url) ";
            const string name = "IsUseProxy_Regex";
            return ParseAs(rules, format, name);
        }

        //IsUseProxy_StartWith
        private static string ParseAsStartWith(Dictionary<string, bool> rules)
        {
            const string format = @"    || shExpMatch(url, ""{0}*"")";
            const string name = "IsUseProxy_StartWith";
            return ParseAs(rules, format, name);
        }

        //IsUseProxy_ContainsWith
        private static string ParseAsContainsWith(Dictionary<string, bool> rules)
        {
            const string format = @"    || shExpMatch(url, ""*{0}*"")";
            const string name = "IsUseProxy_ContainsWith";

            return ParseAs(rules, format, name);
        }

        //转换普通 URL
        private static string ParseAs(Dictionary<string, bool> rules, string format, string name)
        {
            //const string format = @"    || shExpMatch(url, ""*{0}*"")";
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("function {0}(url, host)", name);
            sb.AppendLine();
            sb.AppendLine("{");
            sb.AppendLine("    url = url.toLocaleLowerCase();");
            sb.AppendLine();
            var items = rules.Where(o => o.Value == false).Select(o => o.Key).ToArray();
            string type = false.ToString().ToLower();
            if (items.Length > 0)
            {
                sb.AppendLine("    //白名单");
                sb.AppendLine("    if( 0  ");
                foreach (string item in items)
                {
                    sb.AppendFormat(format, item.Trim().ToLower());
                    sb.AppendLine();
                }
                sb.AppendLine("   )");
                sb.AppendLine("        return -1;");
            }
            items = rules.Where(o => o.Value).Select(o => o.Key).ToArray();
            type = true.ToString().ToLower();
            if (items.Length > 0)
            {
                sb.AppendLine("    //黑名单");
                sb.AppendLine("    if( 0  ");
                foreach (string item in items)
                {
                    sb.AppendFormat(format, item.Trim().ToLower());
                    sb.AppendLine();
                }
                sb.AppendLine("   )");
                sb.AppendLine("        return 1;");
                sb.AppendLine();
            }
            sb.AppendLine();
            sb.AppendLine("    return 0;");
            sb.AppendLine("}");
            return sb.ToString();
        }

        #region Data
        private static string Head1 = @"function decode64(_1){var _2=""ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="";var _3="""";var _4,_5,_6;var _7,_8,_9,_a;var i=0;_1=_1.replace(/[^A-Za-z0-9\+\/\=]/g,"""");do{_7=_2.indexOf(_1.charAt(i++));_8=_2.indexOf(_1.charAt(i++));_9=_2.indexOf(_1.charAt(i++));_a=_2.indexOf(_1.charAt(i++));_4=(_7<<2)|(_8>>4);_5=((_8&15)<<4)|(_9>>2);_6=((_9&3)<<6)|_a;_3=_3+String.fromCharCode(_4);if(_9!=64){_3=_3+String.fromCharCode(_5);}if(_a!=64){_3=_3+String.fromCharCode(_6);}}while(i<_1.length);return _3;}eval(decode64(""";
        private static string Head2 = @"""))";

        private const string IsDomain = @"var hLen = 0; // host长度
function IsDomain(host, domain) 
{
    var dLen = domain.length;
    if (hLen > dLen) 
    {
        return (host.substring(hLen - dLen - 1) == ""."" + domain);
    }
    return (host == domain);
}";

        private const string FindProxyForURL = @"function FindProxyForURL(url, host) {
    var PROXY = ""PROXY_TAG"";
    var DEFAULT = ""DIRECT"";
    var state = 0;
    hLen = host.length;
    do
    {
        state = IsUseProxy_Domain(url, host);
        if(state != 0)
            break;

        state = IsUseProxy_Regex(url, host);
        if(state != 0)
            break;

        state = IsUseProxy_StartWith(url, host);
        if(state != 0)
            break;

        state = IsUseProxy_ContainsWith(url, host);
        if(state != 0)
            break;

    }while(false);
    if(state == 1)
    {
        return PROXY;
    }
    return DEFAULT;
}
";
        #endregion
    }
}
